概述
本节完成学习列表页面的开发,页面包含推荐课程、每日一课、精品微课、学习计划、优质专栏五个模块。涉及 Grid 布局、Tabs 组件封装、defineModel 双向绑定、line-clamp 文本截断等核心技术。
1. 页面结构总览
学习列表页由以下模块组成:
<template>
<div class="container">
<!-- 1. 推荐课程 -->
<section>推荐内容 - Card 网格布局</section>
<!-- 2. 每日一课 -->
<section>Tabs + Grid 不等分布局</section>
<!-- 3. 精品微课 -->
<section>Card 网格布局(8 个课程)</section>
<!-- 4. 学习计划 -->
<section>图片卡片 + 遮罩层</section>
<!-- 5. 优质专栏 -->
<section>Card 网格布局(8 个课程)</section>
</div>
</template>
vue
2. 推荐课程模块
2.1 数据结构
interface CourseItem {
id: number
title: string
image: string
teacher: string
tag: string
count: number // 学习人数
price: string // 价格
}
const recommend = ref<CourseItem[]>([
{
id: 1,
title: 'Vue3 企业级实战',
image: '/images/course-1.jpg',
teacher: '张老师',
tag: 'Vue3',
count: 2000,
price: '399.00',
},
// ... 更多数据
])
ts
2.2 Card 组件复用
推荐课程复用已有的 Card 组件,增加价格和购物车图标:
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div v-for="item in recommend" :key="item.id" class="card">
<img :src="item.image" class="w-full h-40 object-cover rounded-t" />
<div class="p-2">
<h3 class="font-bold line-clamp-2 mb-2">{{ item.title }}</h3>
<div class="flex justify-between items-center">
<span class="text-sky-400 font-bold">{{ item.price }}</span>
<span class="text-sm text-gray-500">{{ item.count }}人学习</span>
</div>
<div class="flex justify-between items-center mt-2">
<span class="text-sm text-gray-600">{{ item.teacher }}</span>
<span class="text-sky-400 text-3xl cursor-pointer">
<span class="iconify" data-icon="ep:shopping-cart" />
</span>
</div>
</div>
</div>
</div>
vue
2.3 line-clamp 文本截断
当课程标题过长时,使用 line-clamp 限制显示行数:
/* 在 Card 组件中 */
.title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
css
对应 UnoCSS 类名:line-clamp-2。
注意:使用 line-clamp 时不能同时使用 padding 方案控制间距,应改用 margin-bottom。
3. Tabs 组件封装
3.1 组件定义
由于 Tabs 在详情页和列表页都会使用,将其封装为独立组件 components/Tabs.vue:
<script setup lang="ts" generic="T">
const props = defineProps<{
items: T[]
}>()
const activeIndex = defineModel<number>('activeIndex', {
default: 0,
local: true, // 允许本地赋值
})
const isActive = (index: number) => index === activeIndex.value
</script>
<template>
<ul class="tabs">
<li
v-for="(item, index) in items"
:key="index"
:class="{ active: isActive(index) }"
@click="activeIndex = index"
>
{{ item }}
</li>
</ul>
</template>
vue
3.2 defineModel 双向绑定
defineModel 是 Vue 3.3+ 引入的宏,简化了 v-model 的双向绑定实现:
// 传统方式:需要 defineProps + defineEmits
const props = defineProps<{ modelValue: number }>()
const emit = defineEmits<{ 'update:modelValue': [value: number] }>()
// defineModel 方式:一行代码替代
const activeIndex = defineModel<number>({ default: 0, local: true })
ts
| 参数 | 说明 |
|---|---|
default: 0 | 默认激活第一个 Tab |
local: true | 允许组件内部直接修改值(即使父组件未传递 v-model) |
前提:需在 vite.config.ts 或 env.d.ts 中开启实验特性:
// vite.config.ts
define: {
__VUE_DEFINE_MODEL__: true,
}
ts
3.3 泛型支持
使用 generic="T" 为组件添加泛型,使 items 可以接受任意类型的数组:
<script setup lang="ts" generic="T">
defineProps<{
items: T[]
}>()
</script>
vue
TypeScript 会自动根据传入的数据推导 T 的具体类型。
3.4 使用 Tabs 组件
<!-- 列表页:每日一课 -->
<Tabs
:items="['Java', 'Vue', 'React', '小程序']"
v-model:active-index="dailyActive"
/>
<!-- 详情页:课程导航 -->
<Tabs
:items="['课程介绍', '章节目录', '学员评价']"
v-model:active-index="courseActive"
/>
vue
4. 学习计划模块 -- 图片遮罩效果
学习计划模块使用图片卡片 + 半透明遮罩 + 居中标题的布局:
<div class="grid grid-cols-4 gap-3">
<div
v-for="(item, index) in recommend"
:key="index"
class="relative overflow-hidden rounded-md group cursor-pointer"
>
<!-- 背景图片 -->
<div
class="w-full h-32 bg-cover bg-center"
:style="{ backgroundImage: `url(${item.image})` }"
/>
<!-- 半透明遮罩 -->
<div class="absolute inset-0 bg-black/40 group-hover:hidden" />
<!-- 居中标题 -->
<div class="absolute inset-0 flex items-center justify-center z-30 px-4">
<h3 class="text-white text-2xl line-clamp-3 text-center">
{{ item.title }}
</h3>
</div>
</div>
</div>
vue
遮罩 Hover 交互
使用 UnoCSS 的 group 和 group-hover: 实现鼠标悬停时移除遮罩:
/* group-hover:hidden 的工作原理 */
/* 鼠标悬停在父级(group)上时,子元素隐藏 */
css
group 类标记父容器,group-hover: 前缀控制子元素在父级 hover 时的行为。
5. 每日一课 -- Grid 不等分布局
每日一课模块使用 Grid 实现左侧大图 + 右侧八宫格的不等分布局。
5.1 Flex 方案
<div class="flex gap-4">
<!-- 左侧:占 1/3 -->
<div class="w-1/3">
<div class="h-full bg-gray-200">
<!-- 特色课程大图 -->
</div>
</div>
<!-- 右侧:占 2/3 -->
<div class="w-2/3">
<div class="grid grid-cols-4 grid-rows-2 gap-3">
<!-- 8 个课程卡片 -->
</div>
</div>
</div>
vue
5.2 Grid 方案
<div class="grid grid-cols-6 gap-4">
<!-- 左侧:占 2 列 -->
<div class="col-start-1 col-span-2 row-span-2">
<!-- 特色课程 -->
</div>
<!-- 右侧:占 4 列 -->
<div class="col-start-3 col-span-4">
<div class="grid grid-cols-4 grid-rows-2 gap-3">
<!-- 8 个课程卡片 -->
</div>
</div>
</div>
vue
| 属性 | 说明 |
|---|---|
grid-cols-6 | 将网格分为 6 等份 |
col-start-1 | 从第 1 条网格线开始 |
col-span-2 | 跨越 2 列 |
row-span-2 | 跨越 2 行 |
6. Mock 数据与测试图片
在开发阶段使用本地测试图片作为占位:
import bg from '@/assets/images/bg.png'
// 将所有课程的 image 统一替换为测试图片
const mockData = recommend.value.map(item => ({
...item,
image: bg,
}))
ts
后续对接接口时替换为真实数据即可。
小结
| 技术 | 应用场景 |
|---|---|
defineModel | Tabs 组件双向绑定 activeIndex |
generic 泛型 | Tabs 接受任意类型的 items 数组 |
line-clamp-* | 课程标题多行截断 |
group / group-hover: | 学习计划遮罩 hover 交互 |
| Grid 不等分 | 每日一课左侧大图 + 右侧八宫格 |
| Flex 百分比 | w-1/3 + w-2/3 替代 Grid 方案 |
↑